Skip to content

fix(cli): probe dev ports without the dnt Deno shim - #3599

Closed
kojiwakayama wants to merge 3 commits into
mainfrom
fix/dx-20260811-r2-0
Closed

fix(cli): probe dev ports without the dnt Deno shim#3599
kojiwakayama wants to merge 3 commits into
mainfrom
fix/dx-20260811-r2-0

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The symptom

On published 0.1.1229, veryfront dev never binds a port when the CLI runs on Deno — the Deno-global install (deno install -gArf npm:veryfront), deno run -A npm:veryfront dev, and the deno task dev that a --runtime deno scaffold prints as its own next step:

Veryfront (v0.1.1229)

✗ [unknown-error] Unknown/unclassified error

  Detail: Cannot read properties of null (reading 'fd')
  Suggestion: Check logs for more details

build, serve, routes and doctor all succeed under the same install. dev was the only command that reached the broken code, and the npm-global (Node) install of the identical version runs dev fine.

Root cause

The real stack, recovered by calling handleDevCommand directly under Deno against the published tarball:

TypeError: Cannot read properties of null (reading 'fd')
    at Object.listen (node_modules/@deno/shim-deno/dist/deno/stable/functions/listen.js:44:64)
    at isPortAvailable (node_modules/veryfront/esm/cli/commands/dev/port-fallback.js:38:26)
    at clearLocalCachesIfPortFree (node_modules/veryfront/esm/cli/commands/dev/command.js:95:16)

isPortAvailable branched on isDeno and then called the bare Deno.listen. In the npm build dnt rewrites every bare Deno to @deno/shim-denoesm/_dnt.shims.js re-exports the shim unconditionally, so the shim wins even when a genuine Deno global is right there. That shim's Node-backed listen() does:

const waitFor = new Promise((resolve) => server.listen(port, hostname, resolve));
// @ts-expect-error undocumented socket._handle property
const listener = new Listener(server._handle.fd, );

server._handle is assigned synchronously by Node's listen(), but not by Deno's node-compat layer, so it is still null on that line. The one branch written for Deno was the one branch Deno could not execute.

The repo already knows this hazard — cli/auth/callback-server.ts reaches the real namespace via self ("Access native Deno.serve via self to bypass dnt shim transform"), and getNativeDeno() in src/platform/compat/http/native-response.ts documents it. port-fallback.ts, which was written to copy that callback server's fallback, missed it.

The fix

Probe with node:net on every runtime and delete the Deno branch. This function already used node:net on Node; it is one path now, it behaves identically on Deno, and it cannot reach the shim. Verified on all four runtime shapes the CLI ships in: Deno source, deno compile binary, npm-on-Node, npm-on-Deno.

Regression test

cli/commands/dev/port-fallback.test.ts stands a throwing Deno.listen into the ambient namespace — the shim reproduced in-process, same TypeError: Cannot read properties of null (reading 'fd') — and asserts the probe still reports a held port busy and a free port free. It fails on main at port-fallback.ts:41 with exactly the published error, and passes with this change.

Proof against the published artifact, not just the tests

The finding's own command, re-run against veryfront@0.1.1229 with only this function transplanted into the installed tree:

before after
deno run -A ./node_modules/veryfront/bin/veryfront.js dev --port 3772 ✗ [unknown-error] … reading 'fd', nothing listening ✓ Ready in 1.6s / http://veryfront.me:3772, lsof confirms TCP 127.0.0.1:3772 (LISTEN)
same, port already taken ! Port 3772 is in use, using 3773 instead✓ Ready
node ./node_modules/veryfront/bin/veryfront.js dev --port 3774 (control) ✓ Ready, curl 200 ✓ Ready, curl 200

What this does not fix

With the port crash gone, veryfront dev under Deno binds and reports Ready, but page renders still fail with a separate Deno defect: the SSR module loader dynamic-importing generated .cache/veryfront-http-bundle/http-*.mjs throws TypeError: Loading unprepared module: …, so / returns 500 (Node returns 200 on the identical project). That is a different failure in a different subsystem and belongs to the open "CLI is entirely non-functional under the Deno runtime" reports — deliberately out of scope here rather than folded into this change.

Also from the originating finding, and deliberately unchanged: the published package declaring 7 of the 20 @veryfront/ext-* specifiers its runtime references is policy, not a packaging bugrootNpm in src/extensions/first-party-defaults.ts keeps credentialed, native and output-changing integrations out of the root dependency set, and first-party-import.ts now degrades with an install hint instead of dying. No live doc page needs to change for this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Improved development server port detection across supported runtimes.
    • Port availability checks now reliably identify occupied and available ports without depending on ambient runtime behavior.
  • Tests

    • Added regression coverage for port detection when the runtime’s default listener is unavailable.

`veryfront dev` died before binding anything whenever the CLI ran on Deno
(`deno install -g npm:veryfront`, `deno run -A npm:veryfront dev`, or the
`deno task dev` a `--runtime deno` scaffold prints):

    ✗ [unknown-error] Unknown/unclassified error
      Detail: Cannot read properties of null (reading 'fd')

The port probe branched on `isDeno` and then called the bare `Deno.listen`.
In the npm build dnt rewrites that bare global to `@deno/shim-deno`, whose
Node-backed `listen()` reads `server._handle.fd` synchronously - and under
Deno's node compat `_handle` is still null at that point. So the one branch
written for Deno was the one branch Deno could not run, and `dev` was the
only command that reached it.

Probe with `node:net` on every runtime instead. It is the path this function
already used on Node, it behaves identically on Deno (source, npm build and
`deno compile` binary all verified), and it cannot reach the shim.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26fe7db9-2732-47bd-9dd5-85676862d16a

📥 Commits

Reviewing files that changed from the base of the PR and between 48922f0 and 7b9f2ce.

📒 Files selected for processing (2)
  • cli/commands/dev/port-fallback.test.ts
  • cli/commands/dev/port-fallback.ts
📝 Walkthrough

Walkthrough

isPortAvailable now probes ports with node:net in all runtimes. A regression test confirms detection of occupied and available ports when Deno.listen throws.

Changes

Port probing

Layer / File(s) Summary
Replace probing and validate runtime independence
cli/commands/dev/port-fallback.ts, cli/commands/dev/port-fallback.test.ts
isPortAvailable uses node:net, closes successful listeners, handles occupied-port errors, and tests behavior when Deno.listen throws.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: avoiding the dnt Deno shim when probing development ports.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dx-20260811-r2-0

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cli/commands/dev/port-fallback.ts (1)

38-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce duplicated implementation-detail comments.

Keep the runtime reason concise. Move detailed shim failure mechanics to the PR description or an issue if they need long-term retention.

  • cli/commands/dev/port-fallback.ts#L38-L44: reduce the JSDoc to the reason that node:net is used on all runtimes.
  • cli/commands/dev/port-fallback.test.ts#L130-L136: reduce the test comment to the mocked condition and expected behavior.

As per coding guidelines, use “direct, concise, active, present-tense public language.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/dev/port-fallback.ts` around lines 38 - 44, Shorten the JSDoc in
cli/commands/dev/port-fallback.ts:38-44 to state concisely why node:net is used
across all runtimes, removing detailed shim failure mechanics. Also shorten the
test comment in cli/commands/dev/port-fallback.test.ts:130-136 to describe only
the mocked condition and expected behavior, using direct, concise, active,
present-tense language.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cli/commands/dev/port-fallback.ts`:
- Around line 38-44: Shorten the JSDoc in
cli/commands/dev/port-fallback.ts:38-44 to state concisely why node:net is used
across all runtimes, removing detailed shim failure mechanics. Also shorten the
test comment in cli/commands/dev/port-fallback.test.ts:130-136 to describe only
the mocked condition and expected behavior, using direct, concise, active,
present-tense language.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c75453a-cf63-4423-bc10-dd9dc3e10260

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2753d and 48922f0.

📒 Files selected for processing (2)
  • cli/commands/dev/port-fallback.test.ts
  • cli/commands/dev/port-fallback.ts

Carries over the static invariant test from the duplicate PR #3598
alongside the behavioural one already here. They catch different things:

- the behavioural test poisons the ambient `Deno.listen` and asserts the
  probe still answers correctly, proving the fix works when the shim is
  in place;
- this one reads the source and asserts no bare `Deno.<member>` access
  survives anywhere in the file, catching a future reintroduction on
  paths the behavioural test never executes.

Verified red both ways before landing: against the pre-fix source both
tests fail, and with a bare `Deno.hostname()` planted in
`findAvailablePort`'s throw path the behavioural test passes while this
one fails naming `Deno.hostname`.

Comments are stripped before matching, since dnt rewrites code and not
prose, and `isPortAvailable`'s doc comment has to stay free to name
`Deno.listen` as the call the fix removed.
Review nitpick. Keeps what a future reader cannot recover from the code -
that dnt swaps the ambient `Deno` for a shim whose `listen` reads a null
`server._handle` - and drops the retelling of the incident around it.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Addressed the nitpick in 7b9f2ce: both comments trimmed to the part a reader cannot recover from the code — dnt swaps the ambient Deno for a shim whose listen reads a null server._handle — with the incident retelling dropped. Kept the mechanism itself rather than moving it to the PR description: the whole defect is that the obvious-looking Deno.listen is the one call this file must not make, and a future editor reading only the source would have no way to know that.

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 11, 2026
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 11, 2026
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #3610, which merged at a7ba48676 and fixes the same crash from the same root-cause analysis.

These were competing implementations of one fix, not two fixes:

Rebasing this onto main is not a conflict resolution but an adjudication between the two, so it was left alone rather than decided unilaterally.

A trap worth recording

cli/commands/dev/port-fallback.test.ts auto-merged with no conflict marker, producing a file that requires the source to contain getDenoRuntime() and to not route through the ambient namespace. No implementation satisfies both. Verified in both directions:

  • main's implementation + merged test → TypeError: Cannot read properties of null (reading 'fd') at port-fallback.ts:51
  • this PR's implementation + merged test → fails assertStringIncludes(code, "getDenoRuntime()")

So a rebase that resolved only the .ts conflict would have shipped a guaranteed-red test file that git reported as clean.

Nothing is lost by closing

The static source-level guard this PR carried (ported from #3598) has an equivalent on main — "probes ports through getDenoRuntime(), not the bare global dnt rewrites", which reads the source the same way. One nuance for anyone revisiting: main's asserts the source contains getDenoRuntime(), whereas the version here asserted it contains no bare Deno. member access. The negative form is slightly stronger — it would catch a bare Deno.hostname() added anywhere in the file, including paths no behavioural test executes.

If the single-path node:net simplification is still wanted, it deserves a fresh PR against current main that also removes #3610's getDenoRuntime() assertion, argued on its own merits.

Branch left in place.

@kwakayama kwakayama left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one low-severity documentation issue.

  • [P3] Update the stale remediation guidance in cli/commands/dev/port-fallback.test.ts:174. It says the safe approach is getDenoRuntime() / Reflect.get(globalThis, "Deno"), but the implementation now deliberately uses import("node:net") in cli/commands/dev/port-fallback.ts:43. This is misleading to future maintainers and could steer a regression fix toward a different, untested path.

Rubric:

  • Correctness: 40/40
  • Tests: 20/20
  • Reliability/security: 15/15
  • Maintainability: 13/15
  • Scope/docs: 8/10

Review-Gate:
Reviewer: Codex
Reviewed-SHA: 7b9f2ce
Score: 96/100
Actionable-Findings: 1
Verdict: COMMENT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants